﻿using System;
using System.Collections.Generic;
using Ur;
using Sargon;

namespace Pixels.Rendering
{
    class AdvancedCamera
    {
        public AdvancedCamera() {            
            init();            
        }

        public const float MAX_INCLINE = 45f;
        const float SCALE_FACTOR = 0.01f;
        //const float DISTANCE = 20f; const float FOV      = 110f;
        //const float DISTANCE = 38f; const float FOV      = 85f;
        const float DISTANCE = 60f; const float FOV = 85f; 
        
        public crds3 project( float x, float y, float z ) {
            var p = this.project(new crds3(x, y, z));
            return p;
        }

        public crds3 unproject( crds2 screenCoords, float z_plane ) {
            return intersectHorizontalPlane(screenCoords, z_plane);
        }
        public crds3 Target {
            get { return target_coords; }
            set { target_coords = value; }
        }

        public void advanceTime( float time ) {        
            prepare_frame();
        }


        #region Private states
        private crds3 _angles;
        private crds3 _center;
        private float _scale;
        private float _distance;
        private float _fov = FOV;

        private crds3 _delta;
        private crds3 _torque;
        private float _dscale;
        private float _ddist; // delta distance
        public  float _dfov;

        private float _minscale;
        private float _maxscale;

        private float _mindist;
        private float _maxdist;

        public crds3 Angles { get { return _angles; } }


        #endregion

        #region Externally manipulable vars
        internal crds3 target_coords;
        internal crds3 target_angles { get { return currentRegime.Angles; } set { currentRegime.Angles = value; } }
        internal float target_scale = 1f;
        internal float target_distance { get { return currentRegime.Distance; } set { currentRegime.Distance = value;}}
        internal float target_fov { get { return currentRegime.FOV; } set { currentRegime.FOV = value; } }
        internal float alacrity = 0.36f;

        internal crds3 rotation_constraints_lo;
        internal crds3 rotation_constraints_hi;

        internal float zoom {
            get {
                //if (perspective) 
                return _eye.z / Screen.w2;
                //return _scale + _dscale * Sargon.Game.interpolation;
            }
        }

        internal float distance { get { return _distance + _ddist * Sargon.Game.interpolation; } }
        #endregion

        #region Getters

        public crds3 center {
            get {
                return _center + _delta * Sargon.Game.interpolation;
            }
        }

        #endregion

        #region Manipulations

        internal void set_distance_constraints( float lo, float hi ) { _mindist = lo; _maxdist = hi; }

        internal void set_scale_constraints( float lo, float hi ) { _minscale = lo; _maxscale = hi; }
        internal void jump() {

            _center = target_coords;
            _angles = target_angles;
            _scale = target_scale;
            _distance = target_distance;
            _fov = target_fov;

            _torque = crds3.zero;
            _delta = crds3.zero;
            _dscale = 0f;
            _ddist = 0f;
            _dfov = 0f;

        }

        #endregion

        #region Initialization
        public void init() {

            rotation_constraints_lo.set(float.MinValue, float.MinValue, float.MinValue);
            rotation_constraints_hi.set(float.MaxValue, float.MaxValue, float.MaxValue);

            initRegimes();            

            _minscale = 1f;
            _maxscale = 10f;
            _mindist = 1f;
            _maxdist = 1000f;

            target_scale = _scale = 10f;

            //perspective = true;
        }
        #endregion

        #region Projections

        private crds3 project( crds3 world ) {

            if ( _rotmatrix == null ) prepare_frame();
            crds3 b = crds3.zero;
            crds3 f = _center + _delta * Sargon.Game.interpolation; // f is the world position of the camera focus
            var   s = _scale + _dscale * Sargon.Game.interpolation; // s is scale factor (more on that later)
            var   e = _eye;
            var c = f + _offset;
            var d = ( crds3 )( _rotmatrix * ( NMatrix )( world - c ) ); // d is world vector in camera's coordinate system
            b.x = Screen.w2 + ( d.x - e.x ) * ( e.z / d.z );
            b.y = Screen.h2 + ( d.y - e.y ) * ( e.z / d.z );
            //b.z = ( e.z / d.z ) * SCALE_FACTOR;
            b.z = 1f * SCALE_FACTOR; // orthogonal
            return b;
        }

        // intersect line

        private crds3 intersectHorizontalPlane( crds2 rayScreen, float worldZ ) {
            if ( _rotmatrix == null ) prepare_frame();
            var p = center + _offset;
            var qq = new crds3(rayScreen.x - Screen.w2, rayScreen.y - Screen.h2, _eye.z) * ( 1f / _eye.z );
            qq = _rotmatrix.inverse3x3() * ( NMatrix )qq;
            qq.normalize();
            var n = ( worldZ - p.z ) / qq.z;
            return p + qq * n;

        }

        #endregion

        #region Renders and ticks
        // Sargon engine tick
        public void tick() {

            // set constraints:

            target_angles = new crds3(
                target_angles.x.choke(rotation_constraints_lo.x, rotation_constraints_hi.x),
                target_angles.y.choke(rotation_constraints_lo.y, rotation_constraints_hi.y),
                target_angles.z.choke(rotation_constraints_lo.z, rotation_constraints_hi.z));

            target_scale = target_scale.choke(_minscale, _maxscale);
            target_distance = target_distance.choke(_mindist, _maxdist);

            // apply deltae :
            _center += _delta; if ( _center.dist_squared(target_coords) < 0.0001f ) _center = target_coords;
            _angles += _torque; if ( _angles.dist_squared(target_angles) < 0.0001f ) _angles = target_angles;
            _scale += _dscale;
            _distance += _ddist;
            _fov += _dfov;

            // calculate next deltae:
            _delta = ( target_coords - _center ) * alacrity * 0.7f;
            _dscale = ( target_scale - _scale ) * alacrity;
            _ddist = ( target_distance - _distance ) * alacrity;
            _dfov = ( target_fov - _fov ) * alacrity;

            if ( _dscale > 10f ) _dscale = 10f;

            _torque = ( target_angles - _angles ) * alacrity;

        }

        private NMatrix _rotmatrix;
        private crds3 _offset;
        private crds3 _eye;

        public void prepare_frame() {
            // determine matrices
            var angles = _angles + _torque * Sargon.Game.interpolation;

            angles.x = 180 - angles.x;

            var cx = Maths.cos(angles.x); var cy = Maths.cos(angles.y); var cz = Maths.cos(angles.z);
            var sx = Maths.sin(angles.x); var sy = Maths.sin(angles.y); var sz = Maths.sin(angles.z);
            var rotx = new NMatrix(3, 3); rotx.setup(1, 0, 0, 0, cx, -sx, 0, sx, cx);
            var roty = new NMatrix(3, 3); roty.setup(cy, 0, sy, 0, 1, 0, -sy, 0, cy);
            var rotz = new NMatrix(3, 3); rotz.setup(cz, -sz, 0, sz, cz, 0, 0, 0, 1);
            _rotmatrix = rotx * roty * rotz;

            var cameraDistToFocus = _distance + _ddist * Sargon.Game.interpolation;
            var camTheta = angles.z;
            var camPhi   = 180 - angles.x;
            var cp = Maths.cos(camPhi); var ct = Maths.cos(camTheta);
            var st = Maths.sin(camTheta); var sp = Maths.sin(camPhi);
            _offset = new crds3(-st * sp, -ct * sp, cp) * cameraDistToFocus;

            _eye = new crds3(0, 0, Screen.w2 * 1f / ( Maths.tan(_fov / 2) ));

        }

        public CameraRegime currentRegime;        

        #endregion

        #region Regimes

        public enum Types {
            Normal, 
            SkyEye,
            ShotSelection,
            Count
        }
        public void selectRegime(Types t) {
            currentRegime = _regimes[(int)t];
            rotation_constraints_hi = new crds3(currentRegime.MaxIncline, 0, int.MaxValue);
            rotation_constraints_lo = new Ur.crds3(currentRegime.MinIncline, 0, int.MinValue);
        }
        private CameraRegime[] _regimes;
        void initRegimes() {
            _regimes = new CameraRegime[(int)Types.Count];
            _regimes[(int)Types.Normal] = new CameraRegime();
            _regimes[(int)Types.Normal].setParameters(85, 40, 30, 50, 0, 13, 0, 45);
            _regimes[(int)Types.SkyEye] = new CameraRegime();
            _regimes[(int)Types.SkyEye].setParameters(85, 60, 50, 80, 0, 2, 0, 80);
            _regimes[(int)Types.ShotSelection] = new CameraRegime();
            _regimes[(int)Types.ShotSelection].setParameters(85, 60, 30, 80, 0, 0, 0, 45);
            selectRegime(Types.Normal);
        }

        #endregion
    }

    public class CameraRegime
    {

        private float distMin = 10;
        private float distMax = 100;
        private float max_incline = 45;
        private float min_incline = 0;

        private float defaultFOV = 80;
        private float defaultDistance = 50;
        private crds3 defaultAngles;

        public void reset() {
            FOV = defaultFOV;
            Distance = defaultDistance;
            Angles = defaultAngles;
        }

        public float FOV { get; set; }

        private float _distance;
        public float Distance { get { return _distance; } set { _distance = value.choke(distMin, distMax); } }
        public crds3 Angles { get; set; }
        public float MaxIncline { get { return max_incline; } }
        public float MinIncline { get { return min_incline; } }

        public void setParameters(float FOV, float distance, float distanceMin, float distanceMax, float defaultTheta, float defaultPhi, float minIncline, float maxIncline) {
            defaultFOV = FOV;
            defaultDistance = distance;
            distMax = distanceMax;
            distMin = distanceMin;
            this.max_incline = maxIncline;
            this.min_incline = minIncline;
            defaultAngles = new crds3(defaultPhi, 0, defaultTheta);
            reset();
        }
    }
    
}
